package org.smart4j.framework.common.util;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;

import java.io.File;
import java.io.IOException;
import java.net.JarURLConnection;
import java.net.URL;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

/**
 * 自定义类加载器工具类
 *
 * @author YooLin1c
 * @version 1.0.0
 * @since 2018-01-29 15:35
 */
@Slf4j
public final class ClassUtil {
    /**
     * 获取类加载器
     *
     * @return 类加载器
     */
    public static ClassLoader getClassLoader() {
        //通过当前线程获取类加载器
        return Thread.currentThread().getContextClassLoader();
    }

    /**
     * 根据指定类名加载类
     * tip 为了提高加载类的性能，可将loadClass方法的isInitialized参数置为false
     *
     * @param className     类名
     * @param isInitialized 是否初始化类（初始化：执行类中的静态代码块）
     * @return 加载后的类
     */
    public static Class<?> loadClass(String className, boolean isInitialized) {
        Class<?> clazz;
        try {
            //加载类
            clazz = Class.forName(className, isInitialized, getClassLoader());
        } catch (ClassNotFoundException e) {
            log.error("load class failed!", e);
            throw new RuntimeException(e);
        }
        return clazz;
    }

    /**
     * 获取指定包名下的所有类
     *
     * @param packageName 包名
     * @return 包名下的所有类
     */
    public static Set<Class<?>> getClassSet(String packageName) {
        Set<Class<?>> classSet = new HashSet<>();
        //根据包名将其转换为文件名路径
        try {
            Enumeration<URL> urls = getClassLoader().getResources(packageName.replace(".", "/"));
            while (urls.hasMoreElements()) {
                URL url = urls.nextElement();
                if (Objects.isNull(url)) {
                    continue;
                }
                //获取文件类型
                String protocol = url.getProtocol();
                //如果是文件类型
                if ("file".equals(protocol)) {
                    //去除路径中的空格
                    String packagePath = url.getPath().replaceAll("%20", "");
                    addClass(classSet, packagePath, packageName);
                    //如果是jar包类型，加载jar包中所有的字节码文件并保存
                } else if ("jar".equals(protocol)) {
                    JarURLConnection jarURLConnection = (JarURLConnection) url.openConnection();
                    if (Objects.isNull(jarURLConnection)) {
                        continue;
                    }
                    JarFile jarFile = jarURLConnection.getJarFile();
                    if (Objects.isNull(jarFile)) {
                        continue;
                    }
                    Enumeration<JarEntry> jarEntries = jarFile.entries();
                    //遍历jar包中的所有文件
                    while (jarEntries.hasMoreElements()) {
                        JarEntry jarEntry = jarEntries.nextElement();
                        String jarEntryName = jarEntry.getName();
                        //字节码文件
                        if (jarEntryName.endsWith(".class")) {
                            //将字节码文件名转为文件路径
                            String className = jarEntryName.substring(0, jarEntryName.lastIndexOf(".")).replaceAll("/", ".");
                            doAddClass(classSet, className);
                        }
                    }
                }
            }
        } catch (IOException e) {
            log.error("get class set from ".concat(packageName).concat(" failed!"), e);
            throw new RuntimeException(e);
        }
        return classSet;
    }

    /**
     * 加载指定类名的类并将其添加至加载类集合中
     *
     * @param classSet  加载类集合
     * @param className 指定类名
     */
    private static void doAddClass(Set<Class<?>> classSet, String className) {
        Class<?> clazz = loadClass(className, false);
        classSet.add(clazz);
    }

    /**
     * 递归加载包路径下的所有字节码文件（包括子包下的）
     *
     * @param classSet    加载类集合
     * @param packagePath 包路径
     * @param packageName 包名
     */
    private static void addClass(Set<Class<?>> classSet, String packagePath, String packageName) {
        //获取包路径下的字节码文件与子文件夹
        File[] files = new File(packagePath).listFiles(
                file -> (file.isFile() && file.getName().endsWith(".class")) || file.isDirectory()
        );
        for (File file : files) {
            String fileName = file.getName();
            //如果是字节码文件，获取并保存其Class对象信息
            if (file.isFile()) {
                //获取字节码文件名
                String className = fileName.substring(0, fileName.lastIndexOf("."));
                //拼接成全限定名
                if (StringUtils.isBlank(className)) {
                    continue;
                }
                className = packageName.concat(".").concat(className);
                doAddClass(classSet, className);
                //如果是子文件夹，递归获取并保存其下所有字节码文件的Class对象信息
            } else {
                String subPackagePath = fileName;
                if (StringUtils.isBlank(subPackagePath)) {
                    continue;
                }
                subPackagePath = packagePath.concat("/").concat(subPackagePath);
                String subPackageName = fileName;
                subPackageName = packageName.concat(".").concat(subPackageName);
                //递归
                addClass(classSet, subPackagePath, subPackageName);
            }
        }
    }
}
